glibc malloc原理简析
1 概述
内存分配器ptmalloc,即glibc中的malloc,实现了 malloc(),free()以及一组其它的函数,以提供动态内存管理的支持。分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序。 为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过某种算法管理这块内存。来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。
2 malloc数据结构
2.1 内存管理结构
2.1.1 chunk
chunk原意是块,用在内存中表示的意思就是一块内存,chunk是glibc内存管理的最小单位,其数据结构如下图所示
chunk中几个关键的成员有prev_size、mchunk_size、fd和bk,其作用分别为:
prev_size:
如果前一个chunk是free chunk,则这个内容保存的是前一个chunk的大小。 如果前一个chunk是allocated chunk,则这个区域保存的是前一个chunk的用户数据;
mchunk_size:
当前chunk的大小。最后的 3 位作为标志位,具体为:
第0比特位用于表示前一个chunk是否为allocated chunk,而当前是不是chunk allocated可以通过查询下一个chunk的这个标志位来得知;
第1比特位用于标记该chunk是否是通过系统调用申请的(子线程是mmap,主线程则是通过 brk)。如果是,则该chunk不再由后续将会介绍的内存管理数据结构来标记,申请释放流程将简化;
第2比特位用于标记该chunk是否属于主分配区,关于分配区将在下文详细介绍。
fd:
前向指针,即指向当前chunk在同一个bin的下一个chunk的指针,仅chunk未使用的时候存在。
bk:
后向指针,即指向当前chunk在同一个bin的上一个chunk的指针,仅chunk未使用的时候存在。
2.1.2 arena
arena一般称为分配区,是一个结构体,内含指向各自类型内存块的指针等元素,每个线程在申请内存时会获取一个。分配区分为主分配区和thread分配区,前者仅有一个,其余均为thread分配区。当新创建的线程需要申请内存时,将从一个全局的链表中获取一个空闲的分配区,如果没有得到且分配区数量没有超过最大值(M_ARENA_MAX),malloc将会新建一个。
2.2.3 heap
heap包括帧头和内存块, glibc以heap为单位从操作系统批量申请和释放内存。 主分配区有一个heap,thread分配区在刚创建时也只有一个,当超过一定大小时会新增heap,heap直接以链表形式相连,数量没有限制,单个heap最大默认64M。新建heap时里面只有一个chunk,称为top chunk,每次申请内存时都会从top chunk中分裂出一块chunk,而top chunk本身则始终位于heap的末端。
下图是只有一个heap的main arena和thread arena的内存分布图: 下图是一个thread arena中含有多个heap的情况: 从以上两图可以看出,thread arena只含有一个arena,却有两个heap_info(即 heap header)。由于两个heap是通过mmap从操作系统申请的内存,两者在内存布局上并不相邻而是分属于不同的内存区间,所以为了便于管理,glibc的malloc将第二个heap_info结构体的prev成员指向了第一个heap_info结构体的起始位置(即ar_ptr成员),而第一个heap_info结构体的ar_ptr成员指向了arena,这样就构成了一个单链表,方便后续管理。
2.2 内存管理链表
glibc提供了几种链表来管理不同大小的chunk。其中,除tcache外,其余均为arena结构体中的成员变量。
2.2.1 tcache
tcache是glibc为了提升小块内存申请释放性能引入的缓存机制。单个tcache有64个链表项,每一项里面最多可保存7块大小相同的chunk,tcache链表本身的数据结构从分配区管理的heap中申请,线程退出时释放回原heap,由于tcache是线程变量,每个线程都会有一个自己的tcache,因此理论上数量无上限。
2.2.2 fastbin
fastbin为管理小块chunk(64位为160字节)的链表,应对频繁申请小块内存的场景。链表项管理的chunk值按一定规律递增,可通过一定的算法算出指定大小的chunk所在的链表项索引,从而找到对应大小的chunk。
2.2.3 unsortedbin
fastbin中整合的chunk和small chunk、 large chunk free之后的chunk被放入unsortedbin,加速内存申请释放,unsortedbin管理的chunk值无规律。
2.2.4 smallbin、largebin
smallbin和largebin管理的chunk值按一定规律递增,可通过一定的算法算出指定大小的chunk所在的链表项索引,从而找到对应大小的chunk。
3 malloc原理分析
3.1 malloc缓存模型
为了兼顾性能和内存占用,glibc的malloc通过一系列的内存管理链表实现了一套复杂的内存缓存机制,其基本思想可以由如下两图概括。 可以看出,当用户需要申请内存时,malloc会先向操作系统申请一个heap,然后对该heap进行切分,根据切分后的内存块的大小交由不同的链表管理。在后续内存申请时,会按照tcache→fastbin→unsortedbin→smallbin/largebin的顺序来获取内存。内存释放过程则与此相反。
3.2 malloc工作流程
3.2.1 内存申请流程
3.2.2 内存释放流程
4 参数配置
4.1 参数列表
glibc提供了一系列的可调参数,用户可以通过设置环境变量的方式调节这些参数从而改变malloc的一些行为。
参数名 | 默认值 | 取值范围 | 作用 |
---|---|---|---|
M_MMAP_MAX | 65536 | >= 0 | 使用mmap分配的最大chunk数,取0时,相当于不使用mmap功能 |
M_MMAP_THRESHOLD | 128 * 1024(字节) | 0 ~ 32M | 所有大于该值的chunk都使用mmap分配内存。如果未设置此参数且未禁用动态调整时,该值将会被动态调整,具体表现为如果上次申请的内存大于该值,则该值将随之增大;如果用户手动设置了这一参数,则将同时禁用动态调整,该值始终保持不变。 |
M_TOP_PAD | 0 | - | 内存申请和释放时额外保留的内存量,避免过多的系统调用 |
M_TRIM_THRESHOLD | 128 * 1024(字节) | - | 收缩阈值,当arena的top值超过收缩阈值将触发收缩操作把多余的内存还给操作系统。如果未设置此参数且未禁用动态调整时,该值将会被动态调整,具体表现为当M_MMAP_THRESHOLD更新时,该值随之更新为前者的两倍;如果用户手动设置了这一参数,则将同时禁用动态调整,该值始终保持不变。 |
M_ARENA_MAX | CPU核数 * 8 | - | arena最大数量 |
M_ARENA_TEST | 8 | - | 限制arena数量,只有当进程现有的arena不足且需求量超过M_ARENA_TEST时才会触发修改arena数量上限的动作。如果设置了M_ARENA_MAX,将忽略M_ARENA_TEST |
tcache_count | 7 | >= 0 | 设置tcache的链表bin数量,当取0时,多余的chunk不会放到tcache里面,相当于关闭了tcache,如: export GLIBC_TUNABLES=glibc.malloc.tcache_count=0 |
tcache_unsorted_limit | 0 | >= 0 | 限制tcache从unsorted bin中获取chunk的数量,当取0时,不做限制 |
4.2 使用环境变量设置参数
4.2.1 兼容模式环境变量
这一方式与低于2.26版本的glibc兼容,但是没有tcache相关的设置。 示例:
# export MALLOC_ARENA_MAX=1
4.2.2 tunables模式环境变量
这一方式适用于2.26及以上的glibc版本,默认使用
示例:
# GLIBC_TUNABLES=glibc.malloc.mmap_max=1:glibc.malloc.top_pad=1
5 调测工具
malloc_stats是glibc提供的一个可用统计本进程具体的内存使用情况的接口,精确到字节, malloc_stats()函数声明如下:
#include<stdlib.h>
#include<malloc.h>
void malloc_stats(void);
malloc_stats()可以在编写代码时加入编译宏来使用,也可以在gdb中直接调用。malloc_stats()执行结果如下:
Arena 0: //分配区编号,这里只有一个线程
system bytes = 135168 //本线程从操作系统获得的动态内存,这里是132KB
in use bytes = 1152 //本线程在使用的动态内存,1152字节
Total (incl. mmap): //总的使用情况,各个线程使用动态内存的累加值
system bytes = 135168 //本进程从操作系统获得的动态内存,这里是132KB
in use bytes = 1152 //本进程在使用的动态内存,1152字节
max mmap regions = 0 //使用mmap区域的个数
max mmap bytes = 0 //mmap区域对应内存大小